﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace UlsStudio.Engine.Parsers
{
    /// <summary>
    /// Reads a single ULS file and spawns FileChunkLoader threads to parse contents on seperate threads
    /// </summary>
    public class UlsFileReader
    {
        #region Constructor & Privates

        private readonly UlsLineParser _lineParserInstance = null;
        private readonly UlsDataSet _fileSetLoader = null;
        private readonly List<FileLoadedUlsLogEntry> _fileBuffer = null;
        private readonly FileInfo _fileToLoad = null;

        public UlsFileReader(UlsDataSet fileSetLoader, FileInfo fileToLoad, UlsScanSettings settings)
        {
            this._fileSetLoader = fileSetLoader;
            this._fileToLoad = fileToLoad;
            this._fileBuffer = new List<FileLoadedUlsLogEntry>(Constants.FILE_LINE_BUFFER_COUNT);
            this._lineParserInstance = new UlsLineParser();
            this.Settings = settings;
            Logging.Writeline("\nfileToLoad={0}", fileToLoad.FullName);
        }
        #endregion

        #region Properties

        /// <summary>
        /// Reference to parent FileSetLoader
        /// </summary>
        public UlsDataSet FileSetParent { get { lock (fileSetParentLock) { return _fileSetLoader; } } }
        private object fileSetParentLock = new object();

        /// <summary>
        /// ULS line parser used to process file
        /// </summary>
        public UlsLineParser FileLineParser { get { return _lineParserInstance; } }

        /// <summary>
        /// File to load data from
        /// </summary>
        public FileInfo FileToLoad { get { return _fileToLoad; } }

        /// <summary>
        /// String list contents of file by line
        /// </summary>
        public List<FileLoadedUlsLogEntry> FileBuffer { get { return _fileBuffer; } }

        /// <summary>
        /// Bytes read from file
        /// </summary>
        public long FileDataBytesProcessed
        {
            get { return _fileDataBytesProcessed; }
            set { _fileDataBytesProcessed = value; }
        }
        private long _fileDataBytesProcessed = 0;

        private static int _linesProcessed = 0;
        private object fileStatsCountLockObj = new object();

        /// <summary>
        /// Lines processed in this file by all the section-loaders. ThreadSafe.
        /// </summary>
        public int LinesProcessed 
        { get { return _linesProcessed; }
                                                                                                                            set { _linesProcessed = value; } }

        public UlsScanSettings Settings { get; set; }

        /// <summary>
        /// Used to determine if we should send back data early for 1st hit, for responsiveness
        /// </summary>
        private bool HaveSpawnedFirstThread { get; set; }

        #endregion

        /// <summary>
        /// Load ULS file and call FileChunkLoader as necesary
        /// </summary>
        /// <returns>Number of ULS lines parsed OK</returns>
        public int LoadFile()
        {
            bool foundFileULSHeader = false;

            this.FileDataBytesProcessed = 0;
            this.LinesProcessed = 0;
            int linesParsedCount = 0;       // Number to return

            FileSetJobLoadingStatus thisLoadStatus = ((FileSetJobLoadingStatus)this.FileSetParent.LoadStatuses[this.Settings]);

            // Open file handle
            using (FileStream fs = new FileStream(this.FileToLoad.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                using (StreamReader file = new StreamReader(fs))
                {
                    try
                    {
                        string lineData = string.Empty; int linesReadCount = 0;

                        // Read file-lines in sequentially until end of file or user cancels
                        while (thisLoadStatus.UserCancelledLoad == false && (lineData = file.ReadLine()) != null)
                        {
                            linesReadCount++;

                            // Ignore line if this a header (only check once)
                            bool lineIsHeader = !foundFileULSHeader || lineData.StartsWith("Timestamp");
                            if (!lineIsHeader)
                            {
                                // Process the ULS logfile line
                                lineData = lineData.Trim();

                                try
                                {
                                    ProcessFileLine(lineData);
                                    linesParsedCount++;
                                }
                                catch (LineInvalidException ex)
                                {
                                    Logging.Writeline("UlsFileReader.UlsFileReader - Error reading line - '{0}'", ex.Message);
                                }

                            }
                            else
                                foundFileULSHeader = true;

                            if (linesReadCount % Constants.UPDATE_FILE_LOADEDED_STATS == 0)
                            {
                                Logging.Writeline("UlsFileReader.LoadFile() - Processed {0} lines; read {1} bytes", LoggingLevel.Verbose, 
                                    linesReadCount, file.BaseStream.Position);
                                this.FileDataBytesProcessed = file.BaseStream.Position;
                            }

                        } // end WHILE

                        // Update stats 
                        this.FileDataBytesProcessed = file.BaseStream.Position;

                        // Abort if user cancelled...
                        if (thisLoadStatus.UserCancelledLoad)
                        {
                            Logging.Writeline("UlsFileReader.UlsFileReader - Aborting...");
                            return linesParsedCount;
                        }

                        // Read any more data left after EOF
                        if (this.FileBuffer.Count >= 0)
                        {
                            Logging.Writeline("UlsFileReader.LoadFile() Done - Processed {0} lines; read {1} bytes", linesReadCount, file.BaseStream.Position);
                            SpawnLoader();
                        }

                        // Update progress (if lines have been parsed ok)
                        if (linesParsedCount > 0)
                        {
                            OnUpdateFileLoadProgress();
                        }
                    }
                    finally
                    {
                        if (file != null) file.Close();
                    }

                }   // End of file
            }
            
            // Return number of lines parsed ok
            return linesParsedCount;
        }


        /// <summary>
        /// Converts a line into a loaded ULS object
        /// </summary>
        /// <param name="lineData">Line to process</param>
        /// <param name="previousEntry">Previous log-entry to modify if the line is a fragment</param>
        /// <param name="loadedLog">The loaded log object</param>
        /// <param name="parser">ULS parser to use</param>
        /// <returns>Whether the line was a fragment or not</returns>
        public static bool GetLogEntryFromLine(string lineData, AbstractUlsLogEntry previousEntry, out AbstractUlsLogEntry loadedLog, UlsLineParser parser)
        {
            // Sanity check
            if (string.IsNullOrEmpty(lineData)) throw new LineInvalidException(lineData);

            // What type of line is this?
            bool lineIsContinuationOfPreviousLine = false;
            try
            {
                lineIsContinuationOfPreviousLine = lineData.Substring(Constants.CONTINUATION_POSITION, Constants.CONTINUATION.Length) == Constants.CONTINUATION;
            }
            catch (ArgumentOutOfRangeException)
            {
                throw new LineInvalidException(lineData);
            }
                                

            // Is this line a continuation of a previous line?
            if (lineIsContinuationOfPreviousLine)
            {
                // Add fragment to last message
                ContinueMessageLine(previousEntry, lineData);

                // This is a fragment
                loadedLog = new EmptyUlsLogEntry();
                return false;
            }
            else
            {
                // Add line to read-buffer
                lineData = lineData.Trim();

                // Return loaded log entry
                loadedLog = FileLoadedUlsLogEntry.LoadFromLine(lineData, parser);
                return true;
            }
        }

        /// <summary>
        /// Adds a message fragment to a previous log entry
        /// </summary>
        /// <param name="previousEntry">Last half-complete message</param>
        /// <param name="lineData">ULS file line with fragment from previous line</param>
        private static void ContinueMessageLine(AbstractUlsLogEntry previousEntry, string lineData)
        {
            string messageContinuation = string.Empty;

            // Find where message starts
            int elipseStartLocation = lineData.IndexOf(Constants.ELPISE);
            messageContinuation =
                lineData.Substring(elipseStartLocation + Constants.ELPISE.Length,
                                    lineData.Length - elipseStartLocation - Constants.ELPISE.Length);


            // Grab just the message part to add to previous message...
            // Trim previous elipse (which there always is)
            if (previousEntry == null)
            {
                Logging.Writeline("UlsFileReader.GetLogEntryFromLine - No previous log entry to hook fragment details onto");
            }
            else
            {
                // Trim trailing "..." from last message
                previousEntry.Message = previousEntry.Message.TrimEnd(Constants.ELPISE.ToCharArray());

                // Check for TAB chars in msg fragment - this will mean avoiding including correlation ID in the message fragment
                int lineTabPosition = messageContinuation.IndexOf("\t");
                if (lineTabPosition > -1)
                {
                    // Trim upto TAB char
                    messageContinuation = messageContinuation.Substring(0, lineTabPosition);
                }
                else
                {
                    // Trim trailing "..." from this message (in case this line too is just a continuation
                    messageContinuation = messageContinuation.TrimEnd(Constants.ELPISE.ToCharArray());
                }
                
                // Format
                messageContinuation = UlsLineParser.FormatStackTraceMessages(messageContinuation);

                // Add message segment to last ULS entry
                previousEntry.Message += messageContinuation;
            }
        }

        /// <summary>
        /// Do something with the line in ULS file
        /// </summary>
        private void ProcessFileLine(string lineData)
        {

            AbstractUlsLogEntry log = null;

            // Process line....
            FileLoadedUlsLogEntry previous = null;
            if (this.FileBuffer.Count > 0)
            {
                previous = this.FileBuffer[this.FileBuffer.Count - 1];
            }
            bool haveFullLogEntry = GetLogEntryFromLine(lineData, previous, out log, this.FileLineParser);

            // If line wasn't a fragment, add it
            if (log != null && log is FileLoadedUlsLogEntry)
            {
                this.FileBuffer.Add((FileLoadedUlsLogEntry)log);

                // Spawn new thread if necesary
                if (!this.HaveSpawnedFirstThread)
                {
                    if (this.FileBuffer.Count >= Constants.FILE_LINE_BUFFER_COUNT)
                        SpawnLoader();
                }
                else
                {
                    if (this.FileBuffer.Count >= Constants.FILE_LINE_BUFFER_COUNT_FIRST_HIT)
                        SpawnLoader();

                    this.HaveSpawnedFirstThread = true;
                }
            }
        }

        /// <summary>
        /// Spawn new parser/loader on seperate thread
        /// </summary>
        private void SpawnLoader()
        {
            FileChunkLoader bufferLoader = new FileChunkLoader(new List<FileLoadedUlsLogEntry>(this.FileBuffer), this, this.FileSetParent.InsertCommand);
            bufferLoader.SegmentComplete += backgroundInserter_LoadSegmentComplete;
            bufferLoader.BeginLoadDataChunk();

            this.FileBuffer.Clear();
        }

        /// <summary>
        /// A section of records have finished being inserted into SQL
        /// </summary>
        void backgroundInserter_LoadSegmentComplete(object sender, FileChunkLoader.LoadSegmentCompleteEventArgs e)
        {
            lock (fileStatsCountLockObj)
            {
                int linesProcessed = this.LinesProcessed;
                this.LinesProcessed += e.LinesInserted;
                //Logging.Writeline("bufferLoader_LoadSegmentComplete() for file {0}", this.FileToLoad.FullName);

                // Raise progress event every time a thread finishes inserting records. Bytes processed are recorded in LoadFile()
                OnUpdateFileLoadProgress();
            }
        }

        #region Event Definitions

        internal class FileLoadEventArgs : EventArgs
        {
            public long BytesLoaded { get; set; }
            public int LinesInserted { get; set; }
            public FileInfo File { get; set; }
            public ScanSettings Settings { get; set; }

            public FileLoadEventArgs(FileInfo file, long bytesLoaded, int linesInserted, ScanSettings settings)
            {
                this.File = file;
                this.BytesLoaded = bytesLoaded;
                this.LinesInserted = linesInserted;
                this.Settings = settings;
            }
        }

        internal delegate void UpdateFileLoadProgressDelegate(object sender, FileLoadEventArgs e);
        internal event UpdateFileLoadProgressDelegate UpdateFileLoadProgress;
        private void OnUpdateFileLoadProgress()
        {
            if (UpdateFileLoadProgress != null)
            {
                lock (fileStatsCountLockObj)
                {
                    UpdateFileLoadProgress(this, new FileLoadEventArgs(this.FileToLoad, this.FileDataBytesProcessed, this.LinesProcessed, this.Settings));
                }
            }
        }

        #endregion
    }

    
}
